﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Runtime.Serialization.Formatters.Binary;
using System.Threading;
using System.IO;
using Types;

namespace Server
{
    [Serializable]
    public class UserRequestManager
    {
        private Socket socket;
        private NetworkStream stream;           // network stream
        private BinaryFormatter formatter;      // binary formater
        public string username { get; set; }    // client username
        private bool isLogin;                   // flag to know if user connected
        private Thread userClient;              // thread to handle client
        private DatabaseManager dbm;
        private MainRequestManager mrm;
        private const string questionsTargetPath = @"c:\csharp_project\storage\questions";
        private const string answersTargetPath = @"c:\csharp_project\storage\answers";
        private List<CourseType> courseList;

        public delegate void SendDataToUserDelegate(object data);

        public UserRequestManager(Socket socket, NetworkStream socketStream)
        {
            isLogin = false;
            this.socket = socket;
            this.stream = socketStream;
            try
            {
                stream = new NetworkStream(socket);
            }
            catch (System.ArgumentNullException)
            {
                throw (new ConnectionException("Failed to start listener thread"));
            }
            catch (System.IO.IOException)
            {
                throw (new ConnectionException("Failed to start listener thread"));
            }
            formatter = new BinaryFormatter();
            userClient = new Thread(new ThreadStart(this.Listener));
            userClient.Start();
            dbm = DatabaseManager.Instance;
            mrm = MainRequestManager.Instance;
            courseList = new List<CourseType>();
        }

        private void HandleRequest(object data)
        {
            Request request = data as Request;

            switch (request.requestType)
            {
                case RequestId.Subscribe:
                    HandleSubscribe(request);
                    break;
                case RequestId.Login:
                    HandleLogin(request);
                    break;
                case RequestId.Logout:
                    HandleLogout(request);
                    break;
                case RequestId.GetCourseList:
                    HandleGetCourseList(request);
                    break;
                case RequestId.GetQuestionList:
                    HandleGetQuestionsList(request);
                    break;
                case RequestId.GetMyQuestions:
                    HandleGetMyQuestionsList(request);
                    break;
                case RequestId.GetAnswerList:
                    HandleGetAnswerList(request);
                    break;
                case RequestId.AddQuestion:
                    HandleAddQuestion(request);
                    break;
                case RequestId.DeleteQuestion:
                    HandleDeleteQuestion(request);
                    break;
                case RequestId.AddAnswer:
                    HandleAddAnswer(request);
                    break;
                case RequestId.Search:
                    HandleSearch(request);
                    break;
                case RequestId.GetFileContent:
                    HandleGetFileContent(request);
                    break;
                case RequestId.GetCourseName:
                    HandleGetCourseName(request);
                    break;
                case RequestId.GetQuestionSummary:
                    HandleGetQuestionSummary(request);
                    break;
            }
        }

        private void HandleSubscribe(Request request)
        {
            UserType user = request.objectType as UserType;
            if (!MainRequestManager.Instance.IsOnlineUser(this, user.username) && SubscribeNewUser(user))
            {
                username = user.username;
                request.operationResult = true;
                SendDataToUser(request);
            }
            else
            {
                request.operationResult = false;
                request.objectType = new ErrorMessage("This user is already token");
                SendDataToUser(request);
            }
        }

        private void HandleLogin(Request request)
        {
            UserType user = request.objectType as UserType;
            if (CheckUser(user) != null)
            {
                if (!isLogin && !MainRequestManager.Instance.IsOnlineUser(this, user.username))
                {
                    if (MainRequestManager.Instance.AddToOnlineUsers(this, user.username))
                    {
                        isLogin = true;
                        username = user.username;
                        request.operationResult = true;
                        SendDataToUser(request);
                    }
                }
                else
                {
                    request.operationResult = false;
                    request.objectType = new ErrorMessage("This user is already logged in");
                    SendDataToUser(request);
                }
            }
            else
            {
                request.operationResult = false;
                request.objectType = new ErrorMessage("Input credentials are wrong or subscribe first");
                SendDataToUser(request);
            }
        }

        private void HandleLogout(Request request)
        {
            UserType user = request.objectType as UserType;
            if (isLogin && MainRequestManager.Instance.IsOnlineUser(this, user.username))
            {
                if (MainRequestManager.Instance.RemoveFromOnlineUsers(user.username))
                {
                    isLogin = false;
                    request.operationResult = true;
                    SendDataToUser(request);
                }
            }
            else
            {
                request.operationResult = false;
                request.objectType = new ErrorMessage("This user is already logged out");
                SendDataToUser(request);
            }
        }

        public void HandleServerClosedNotifyUser(Request request)
        {
            UserType user = request.objectType as UserType;
            request.operationResult = true;
            request.objectType = new ErrorMessage("Server is down\nProgram will exit");
            SendDataToUser(request);
        }

        private void HandleGetCourseList(Request request)
        {
            string username = request.objectType as string;
            if ((courseList = GetCourseList(username)) != null)
            {
                request.operationResult = true;
                request.objectType = courseList;
                SendDataToUser(request);
            }
            else
            {
                request.operationResult = false;
                request.objectType = new ErrorMessage("Cannot get course list");
                SendDataToUser(request);
            }
        }

        private void HandleGetQuestionsList(Request request)
        {
            string username = request.objectType as string;
            List<QuestionType> questionList = new List<QuestionType>();
            if ((questionList = GetQuestionList(username)) != null)
            {
                request.operationResult = true;
                request.objectType = questionList;
                SendDataToUser(request);
            }
            else
            {
                request.operationResult = false;
                request.objectType = new ErrorMessage("Cannot get question list");
                SendDataToUser(request);
            }
        }

        private void HandleGetMyQuestionsList(Request request)
        {
            string username = request.objectType as string;
            List<QuestionType> questionList = new List<QuestionType>();
            if ((questionList = GetMyQuestionList(username)) != null)
            {
                request.operationResult = true;
                request.objectType = questionList;
                SendDataToUser(request);
            }
            else
            {
                request.operationResult = false;
                request.objectType = new ErrorMessage("Cannot get user question list");
                SendDataToUser(request);
            }
        }

        private void HandleGetAnswerList(Request request)
        {
            QuestionType question = request.objectType as QuestionType;
            List<AnswerType> answerList = new List<AnswerType>();
            if ((answerList = GetAnswerList(question)) != null)
            {
                request.operationResult = true;
                request.objectType = answerList;
                SendDataToUser(request);
            }
            else
            {
                request.operationResult = false;
                request.objectType = new ErrorMessage("Cannot get answer list");
                SendDataToUser(request);
            }
        }
        
        private void HandleAddQuestion(Request request)
        {
            QuestionType question = request.objectType as QuestionType;
            if (AddQuestion(question))
            {
                request.operationResult = true;
                mrm.SendQuestionToOnlineUsers();
                SendDataToUser(request);
            }
            else
            {
                request.operationResult = false;
                request.objectType = new ErrorMessage("Cannot add question");
                SendDataToUser(request);
            }
        }

        private void HandleDeleteQuestion(Request request)
        {
            QuestionType question = request.objectType as QuestionType;
            if (DeleteQuestion(question))
            {
                request.operationResult = true;
                request.objectType = question;
                mrm.SendQuestionToOnlineUsers();
                SendDataToUser(request);
            }
            else
            {
                request.operationResult = false;
                request.objectType = new ErrorMessage("Cannot delete question");
                SendDataToUser(request);
            }
        }

        private void HandleAddAnswer(Request request)
        {
            AnswerType answer = request.objectType as AnswerType;
            if (AddAnswer(answer))
            {
                request.operationResult = true;
                SendDataToUser(request);
            }
            else
            {
                request.operationResult = false;
                request.objectType = new ErrorMessage("Cannot add answer");
                SendDataToUser(request);
            }
        }

        private void HandleSearch(Request request)
        {
            List<QuestionType> searchResultQuestionList = new List<QuestionType>();
            List<AnswerType> searchResultAnswerList = new List<AnswerType>();
            List<object> searchResultList = new List<object>();

            SearchType search = request.objectType as SearchType;
            switch (search.searchBy)
            {
                case SearchType.By.NoDates:
                    if (SearchNoDates(ref searchResultQuestionList, ref searchResultAnswerList, search))
                    {
                        request.operationResult = true;
                        foreach (var item in searchResultQuestionList)
                        {
                            searchResultList.Add(item);
                        }
                        foreach (var item in searchResultAnswerList)
                        {
                            searchResultList.Add(item);
                        }
                        request.objectType = searchResultList;
                        SendDataToUser(request);
                    }
                    else
                    {
                        request.operationResult = false;
                        request.objectType = new ErrorMessage("Cannot get search result list");
                        SendDataToUser(request);
                    }
                    break;
                case SearchType.By.TwoDates:
                    if (SearchBetweenDates(ref searchResultQuestionList, ref searchResultAnswerList, search))
                    {
                        request.operationResult = true;
                        foreach (var item in searchResultQuestionList)
                        {
                            searchResultList.Add(item);
                        }
                        foreach (var item in searchResultAnswerList)
                        {
                            searchResultList.Add(item);
                        }
                        request.objectType = searchResultList;
                        SendDataToUser(request);
                    }
                    else
                    {
                        request.operationResult = false;
                        request.objectType = new ErrorMessage("Cannot get search result list");
                        SendDataToUser(request);
                    }
                    break;
            }
        }

        private void HandleGetFileContent(Request request)
        {
            string fileContent = null;

            if (request.objectType is QuestionType)
            {
                QuestionType question = request.objectType as QuestionType;
                fileContent = GetFileContent(question.filePath);
                if (fileContent != null)
                {
                    request.operationResult = true;
                    request.objectType = fileContent;
                    SendDataToUser(request);
                }
                else
                {
                    request.operationResult = false;
                    request.objectType = new ErrorMessage("Cannot get file content");
                    SendDataToUser(request);
                }
            }
            else if (request.objectType is AnswerType)
            {
                AnswerType answer = request.objectType as AnswerType;
                fileContent = GetFileContent(answer.filePath);
                if (fileContent != null)
                {
                    request.operationResult = true;
                    request.objectType = fileContent;
                    SendDataToUser(request);
                }
                else
                {
                    request.operationResult = false;
                    request.objectType = new ErrorMessage("Cannot get file content");
                    SendDataToUser(request);
                }
            }
        }

        private void HandleGetCourseName(Request request)
        {
            if (request.objectType is QuestionType)
            {
                QuestionType question = request.objectType as QuestionType;
                string courseName = GetCourseName(question.id);
                if (courseName != null)
                {
                    request.operationResult = true;
                    request.objectType = courseName;
                    SendDataToUser(request);
                }
                else
                {
                    request.operationResult = false;
                    request.objectType = new ErrorMessage("Cannot get course name");
                    SendDataToUser(request);
                }
            }
            else if (request.objectType is AnswerType)
            {
                AnswerType answer = request.objectType as AnswerType;
                string courseName = GetCourseName(answer.questionId);
                if (courseName != null)
                {
                    request.operationResult = true;
                    request.objectType = courseName;
                    SendDataToUser(request);
                }
                else
                {
                    request.operationResult = false;
                    request.objectType = new ErrorMessage("Cannot get course name");
                    SendDataToUser(request);
                }
            }
        }

        private void HandleGetQuestionSummary(Request request)
        {
            AnswerType answer = request.objectType as AnswerType;
            string questionSummary = GetQuestionSummary(answer.questionId);
            if (questionSummary != null)
            {
                request.operationResult = true;
                request.objectType = questionSummary;
                SendDataToUser(request);
            }
            else
            {
                request.operationResult = false;
                request.objectType = new ErrorMessage("Cannot get question summary");
                SendDataToUser(request);
            }
        }

        private UserType CheckUser(UserType user)
        {
            if (dbm.IsLegalUser(user.username, user.password))
            {
                return user;
            }
            return null;
        }

        private bool SubscribeNewUser(UserType user)
        {
            if (dbm.InsertNewUser(user.username, user.password, user.firstName, user.lastName, user.id, user.year))
            {
                return true;
            }
            return false;
        }

        private List<CourseType> GetCourseList(string username)
        {
            if ((courseList = dbm.GetCourseList(username)) != null)
            {
                return courseList;
            }
            return null;
        }

        public List<QuestionType> GetQuestionList(string username)
        {
            List<QuestionType> questionList = new List<QuestionType>();
            if ((questionList = dbm.GetQuestionList(username)) != null)
            {
                return questionList;
            }
            return null;
        }

        public List<AnswerType> GetAnswerList(QuestionType question)
        {
            List<AnswerType> answerList = new List<AnswerType>();
            if ((answerList = dbm.GetAnswerList(question)) != null)
            {
                return answerList;
            }
            return null;
        }

        private List<QuestionType> GetMyQuestionList(string username)
        {
            List<QuestionType> questionList = new List<QuestionType>();
            if ((questionList = dbm.GetMyQuestionList(username)) != null)
            {
                return questionList;
            }
            return null;
        }

        private bool AddQuestion(QuestionType question)
        {
            int questionId = GetQuestionId();
            if (questionId == -1)
            {
                return false;
            }
            question.id = ++questionId;
            if (!AddQuestionFilePath(ref question))
            {
                return false;
            }
            question.summary = question.summary.IndexOf('.') == 0 ? question.summary : question.summary.Split('.')[0];
            if (dbm.InsertNewQuestion(question.id, question.summary, question.filePath, question.date, question.creatorName, question.courseId))
            {
                return true;
            }
            return false;
        }

        private bool AddQuestionFilePath(ref QuestionType question)
        {
            FileInfo fi = null;
            FileStream fs = null;
            StreamWriter sw = null;

            question.filePath = questionsTargetPath + @"\question_" + question.id + ".txt";
            try
            {
                if (!Directory.Exists(questionsTargetPath))
                {
                    Directory.CreateDirectory(questionsTargetPath);
                }
                if (!File.Exists(question.filePath))
                {
                    fi = new FileInfo(question.filePath);
                    fs = fi.Create();
                    sw = new StreamWriter(fs);
                    sw.Write(question.summary);
                }
                else
                {
                    return false;
                }
            }
            catch (System.ArgumentNullException)
            {
                return false;
            }
            catch (System.Security.SecurityException)
            {
                return false;
            }
            catch (System.ArgumentException)
            {
                return false;
            }
            catch (System.UnauthorizedAccessException)
            {
                return false;
            }
            catch (System.IO.PathTooLongException)
            {
                return false;
            }
            catch (System.NotSupportedException)
            {
                return false;
            }
            catch (System.ObjectDisposedException)
            {
                return false;
            }
            catch (System.IO.IOException)
            {
                return false;
            }
            finally
            {
                sw.Close();
                fs.Close();
            }
            return true;
        }

        private int GetQuestionId()
        {
            int questionId = dbm.GetQuestionId();
            return questionId == -1 ? -1 : questionId;
        }

        private bool DeleteQuestion(QuestionType question)
        {
            if (DeleteAnswerFilesPath(question) && DeleteQuestionFilePath(question) && dbm.DeleteQuestion(question))
            {
                return true;
            }
            return false;
        }

        private bool DeleteQuestionFilePath(QuestionType question)
        {
            FileInfo fi = null;

            string questionFilePath = question.filePath;
            if (questionFilePath != null)
            {
                try
                {
                    if (File.Exists(questionFilePath))
                    {
                        fi = new FileInfo(questionFilePath);
                        fi.Delete();
                    }
                    else
                    {
                        return false;
                    }
                }
                catch (System.ArgumentNullException)
                {
                    return false;
                }
                catch (System.Security.SecurityException)
                {
                    return false;
                }
                catch (System.ArgumentException)
                {
                    return false;
                }
                catch (System.UnauthorizedAccessException)
                {
                    return false;
                }
                catch (System.IO.PathTooLongException)
                {
                    return false;
                }
                catch (System.NotSupportedException)
                {
                    return false;
                }
                catch (System.IO.IOException)
                {
                    return false;
                }
            }
            else
            {
                return false;
            }
            return true;
        }

        private bool DeleteAnswerFilesPath(QuestionType question)
        {
            FileInfo fi = null;

            List<string> answerFilesPath = dbm.GetAnswerFilesPath(question);
            if (answerFilesPath != null)
            {
                foreach (var answerFilePath in answerFilesPath)
                {
                    try
                    {
                        if (File.Exists(answerFilePath))
                        {
                            fi = new FileInfo(answerFilePath);
                            fi.Delete();
                        }
                        else
                        {
                            return false;
                        }
                    }
                    catch (System.ArgumentNullException)
                    {
                        return false;
                    }
                    catch (System.Security.SecurityException)
                    {
                        return false;
                    }
                    catch (System.ArgumentException)
                    {
                        return false;
                    }
                    catch (System.UnauthorizedAccessException)
                    {
                        return false;
                    }
                    catch (System.IO.PathTooLongException)
                    {
                        return false;
                    }
                    catch (System.NotSupportedException)
                    {
                        return false;
                    }
                    catch (System.IO.IOException)
                    {
                        return false;
                    }
                }
            }
            else
            {
                return false;
            }
            return true;
        }

        private bool AddAnswer(AnswerType answer)
        {
            int answerId = GetAnswerId();
            if (answerId == -1)
            {
                return false;
            }
            answer.id = ++answerId;
            if (!AddAnswerFilePath(ref answer))
            {
                return false;
            }
            answer.summary = answer.summary.IndexOf('.') == 0 ? answer.summary : answer.summary.Split('.')[0];
            if (!dbm.InsertNewAnswer(answer.id, answer.questionId, answer.summary, answer.filePath, answer.date))
            {
                return false;
            }
            return true;
        }

        private bool AddAnswerFilePath(ref AnswerType answer)
        {
            FileInfo fi = null;
            FileStream fs = null;
            StreamWriter sw = null;

            answer.filePath = answersTargetPath + @"\answer_" + answer.id + ".txt";
            try
            {
                if (!Directory.Exists(answersTargetPath))
                {
                    Directory.CreateDirectory(answersTargetPath);
                }
                if (!File.Exists(answer.filePath))
                {
                    fi = new FileInfo(answer.filePath);
                    fs = fi.Create();
                    sw = new StreamWriter(fs);
                    sw.Write(answer.summary);
                }
                else
                {
                    return false;
                }
            }
            catch (System.ArgumentNullException)
            {
                return false;
            }
            catch (System.Security.SecurityException)
            {
                return false;
            }
            catch (System.ArgumentException)
            {
                return false;
            }
            catch (System.UnauthorizedAccessException)
            {
                return false;
            }
            catch (System.IO.PathTooLongException)
            {
                return false;
            }
            catch (System.NotSupportedException)
            {
                return false;
            }
            catch (System.ObjectDisposedException)
            {
                return false;
            }
            catch (System.IO.IOException)
            {
                return false;
            }
            finally
            {
                sw.Close();
                fs.Close();
            }
            return true;
        }

        private int GetAnswerId()
        {
            int answerId = dbm.GetAnswerId();
            return answerId == -1 ? -1 : answerId;
        }

        private bool SearchNoDates(ref List<QuestionType> searchResultQuestionList, ref List<AnswerType> searchResultAnswerList, SearchType search)
        {
            if (dbm.Search(ref searchResultQuestionList, ref searchResultAnswerList, search.searchCriteria, search.username))
            {
                return true;
            }
            return false;
        }

        private bool SearchBetweenDates(ref List<QuestionType> searchResultQuestionList, ref  List<AnswerType> searchResultAnswerList, SearchType search)
        {
            if (dbm.SearchBetweenDates(ref searchResultQuestionList, ref searchResultAnswerList, search.searchCriteria, search.username, search.startDate, search.endDate))
            {
                return true;
            }
            return false;
        }

        private string GetFileContent(string filePath)
        {
            StreamReader sr = null;
            string fileContent = null;

            if (!File.Exists(filePath))
            {
                return null;
            }
            try
            {
                sr = new StreamReader(filePath);
                fileContent = sr.ReadToEnd();
            }
            catch (System.ArgumentNullException)
            {
                return null;
            }
            catch (System.ArgumentException)
            {
                return null;
            }
            catch (System.OutOfMemoryException)
            {
                return null;
            }
            catch (System.IO.FileNotFoundException)
            {
                return null;
            }
            catch (System.IO.DirectoryNotFoundException)
            {
                return null;
            }
            catch (System.IO.IOException)
            {
                return null;
            }

            finally
            {
                sr.Close();
            }
            return fileContent;
        }

        private string GetCourseName(int questionId)
        {
            string courseName = dbm.GetCourseName(questionId);
            return courseName == null ? null : courseName;
        }

        private string GetQuestionSummary(int questionId)
        {
            string questionSummary = dbm.GetQuestionSummary(questionId);
            return questionSummary == null ? null : questionSummary;
        }

        private void Listener()
        {
            while (socket.Connected)
            {
                try
                {
                    object data = formatter.Deserialize(stream);
                    HandleRequest(data);
                }
                catch (System.IO.IOException)
                {
                    stream.Close();
                    socket.Close();
                }
            }
        }

        public void SendDataToUser(object data)
        {
            try
            {
                lock (this)
                {
                    if (socket.Connected)
                    {
                        formatter.Serialize(stream, data);
                    }
                }
            }
            catch (System.ArgumentNullException)
            {
                throw (new ConnectionException("Failed to send data to client"));
            }
            catch (System.Runtime.Serialization.SerializationException)
            {
                throw (new ConnectionException("Failed to send data to client"));
            }
            catch (System.Security.SecurityException)
            {
                throw (new ConnectionException("Failed to send data to client"));
            }
        }
    }
}
